<?php

namespace liketp;

use ArrayAccess;
use Closure;
use Countable;
use Exception;
use Psr\Container\ContainerInterface;
use ReflectionClass;
use ReflectionFunction;
use ReflectionMethod;
use Throwable;

class Container implements ContainerInterface, ArrayAccess, Countable
{

    protected static $instance;
    protected $bindings = [];
    protected $instances = [];

    /**
     * 构造函数
     * 需要单例运行
     */
    protected function __construct()
    {
        if (self::$instance) {
            throw new \Exception(__CLASS__ . ' 不允许重复实例化');
        }
    }

    protected function __clone()
    {
        throw new \RuntimeException('Cloning is not allowed');
    }

    protected function __wakeup()
    {
        throw new \RuntimeException('Unserializing is not allowed');
    }

    public static function instance()
    {
        static::$instance = static::$instance ?? new static();

        return static::$instance;
    }

    /**
     * 绑定
     * 艾木Five 2025-06-09
     * string    $abstract   绑定标识
     * mixed     $concrete   要绑定的目标，可以是类名、函数、实例 或 其它类型变量
     * return    $this
     */
    public function bind(string $abstract, $concrete)
    {
        if ($concrete instanceof Closure) {
            $this->bindings[$abstract] = $concrete;
        } else if (is_object($concrete)) {
            $this->setInstance($abstract, $concrete);
        } else {
            $abstract = $this->getAlias($abstract);
            if ($abstract != $concrete) {
                $this->bindings[$abstract] = $concrete;
            }
        }
        return $this;
    }

    public function bindMultiple(array $bindings)
    {
        foreach ($bindings as $k => $v) {
            if (is_string($k)) {
                $this->bind($k, $v);
            }
        }
    }

    public function setInstance(string $abstract, object $instance)
    {
        $abstract = $this->getAlias($abstract);
        $this->instances[$abstract] = $instance;
        return $this;
    }

    /**
     * 实例化类
     * 艾木Five 2025-06-09
     */
    public function make(string $abstract, array $arguments = [], bool $newInstance = false)
    {
        $abstract = $this->getAlias($abstract);

        // bind to Obj
        if (isset($this->instances[$abstract]) && $newInstance == false) {
            return $this->instances[$abstract];
        }

        $concrete = $this->bindings[$abstract] ?? null;

        // bind to function
        if ($concrete instanceof Closure) {
            return $this->invokeFunction($concrete, $arguments);
        }
        // bind to base type variable
        else if (is_null($concrete) == false) {
            return $concrete;
        }
        // bind to class name
        else {
            $instance = $this->invokeClass($abstract, $arguments);
        }

        if ($newInstance == false) {
            $this->instances[$abstract] = $instance;
        }

        return $instance;
    }

    public function delete(string $abstract)
    {
        $abstract = $this->getAlias($abstract);

        if (isset($this->instances[$abstract])) {
            unset($this->instances[$abstract]);
        }

        return $this;
    }

    //------------------------------------------------ getter setter
    public function __get($name)
    {
        return $this->get($name);
    }

    public function __set($name, $value)
    {
        $concrete = $this->getAlias($name);
        $this->instances[$concrete] = $value;
    }

    //------------------------------------------------ implements ContainerInterface
    public function get(string $abstract)
    {
        if ($this->has($abstract)) {
            return $this->make($abstract);
        } else {
            throw new Exception('Target of [' . $abstract . '] is not found');
        }
    }

    /**
     * 查询容器中是否存在类、标识 或 实例
     * ak47 2025-06-10
     * 备注： 此函数跟 TP6 中不完全一致，判断更加全面
     */
    public function has(string $abstract): bool
    {
        $bindKeys = array_keys($this->bindings);
        $bindVals = array_values($this->bindings);

        return
            in_array($abstract, $bindKeys) ||
            in_array($abstract, $bindVals) ||
            isset($this->instances[$abstract]);
    }

    //------------------------------------------------ implements ArrayAccess, Countable
    public function offsetExists($offset): bool
    {
        return $this->has(strval($offset));
    }

    public function offsetGet($offset)
    {
        return $this->get(strval($offset));
    }

    public function offsetSet($offset, $value): void
    {
        $this->bind(strval($offset), $value);
    }

    public function offsetUnset($offset): void
    {
        $this->delete(strval($offset));
    }

    public function count(): int
    {
        return count($this->instances);
    }

    //------------------------------------------------ protected
    protected function invokeFunction($fn, array $arguments)
    {
        $reflectFn = new ReflectionFunction($fn);

        $params = $reflectFn->getParameters();
        $args = $this->fillArgs($params, $arguments);

        return $fn(...$args);
    }

    protected function invokeMethod($method, array $arguments)
    {
        if (is_array($method)) {

            [$class, $method] = $method;

            $class = is_object($class) ? $class : $this->make($class);
        } else if (is_string($method)) {

            if (strpos($method, '::')) {
                [$class, $method] = explode('::', $method);
            } else {
                $class = null;
            }
        }

        if (is_null($class)) throw new Exception('class is null');

        try {
            $reflectFn = new ReflectionMethod($class, $method);
        } catch (Throwable $exception) {
            if (method_exists($class, '__call')) {
                $reflectFn = new ReflectionMethod($class, '__call');
                $arguments = [$method, $arguments];
            } else {
                throw $exception;
            }
        }

        $params = $reflectFn->getParameters();
        $args = $this->fillArgs($params, $arguments);

        return $reflectFn->invokeArgs(is_object($class) ? $class : null, $args);
    }

    protected function invokeClass(string $abstract, array $arguments = []): object
    {
        $reflectClass = new ReflectionClass($abstract);

        $constructFn = $reflectClass->getConstructor();

        if ($constructFn == null) {
            $instance = new $abstract();
        } else if ($constructFn->isPublic() == false) {
            throw new \Exception("Class [$abstract] not allow instance.");
        } else {
            $params = $constructFn->getParameters();
            $args = $this->fillArgs($params, $arguments);
            $instance = $reflectClass->newInstanceArgs($args);
        }

        return $instance;
    }

    protected function fillArgs(array $params, array $arguments): array
    {
        $args = [];

        foreach ($params as $param) {
            $varName = $param->getName();
            $varType = $param->getType() ? $param->getType()->getName() : 'mixed';

            if (isset($arguments[$varName])) {
                array_push($args, $arguments[$varName]);
                unset($arguments[$varName]);
                continue;
            }

            if ($this->varTypeIsObj($varType)) {
                if (count($arguments) > 0) {
                    $firstArg = array_shift($arguments);
                    if ($firstArg instanceof $varType) {
                        array_push($args, $firstArg);
                        continue;
                    } else {
                        array_unshift($arguments, $firstArg);
                    }
                }
                array_push($args, $this->make($varType));
                continue;
            } else {
                if (count($arguments) > 0 && is_numeric(array_key_first($arguments))) {
                    $firstArg = array_shift($arguments);
                    array_push($args, $firstArg);
                    continue;
                }

                if ($param->isOptional()) {
                    array_push($args, $param->getDefaultValue());
                } else {
                    throw new \Exception('parameter [' . $varName . '] is missing.');
                }
            }
        }

        return $args;
    }

    /**
     * 判断函数的形参是否是对象实例
     * 艾木Five 2025-06-10
     */
    protected function varTypeIsObj(string $type): bool
    {
        $type = strtolower($type);

        if ($type == 'object') return true;

        $notObjTypes = ['', 'null', 'int', 'float', 'string', 'bool', 'mixed', 'array', 'resource', 'callable'];
        if (in_array($type, $notObjTypes)) {
            return false;
        }

        return true;
    }

    /**
     * 获取绑定链最终的标识
     * 艾木Five 2025-06-10
     * string    $abstract   标识
     * 备注：    此函数的实现方式和 TP6 不完全一样（作用是一样的），增加了防止死循环的处理
     */
    protected function getAlias(string $abstract)
    {
        $i = 0;
        while (isset($this->bindings[$abstract])) {
            $i++;
            if ($i >= 10) {
                throw new \Exception('It seems fall in loop when resolve bingdings [' . $abstract . ']');
            }

            $target = $this->bindings[$abstract];

            if (is_string($target)) {
                $abstract = $target;
            } else {
                break;
            }
        }

        return $abstract;
    }
}
